// Copyright 2019 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.

// This package provides support for gRPC error handling. It has
// two main features:
//
// 1. Automatic en/decoding of gRPC Status errors via EncodeError and
//    DecodeError, enabled by importing the package. Supports both the
//    standard google.golang.org/grpc/status and the gogoproto-compatible
//    github.com/gogo/status packages.
//
// 2. Wrapping arbitrary errors with a gRPC status code via WrapWithGrpcCode()
//    and GetGrpcCode(). There is also a gRPC middleware in middleware/grpc
//    that will automatically do this (un)wrapping.
//
// Note that it is not possible to mix standard Protobuf and gogoproto
// status details via Status.WithDetails(). All details must be standard
// Protobufs with the standard grpc/status package, and all details must
// be gogoproto Protobufs with the gogo/status package. This is caused by
// WithDetails() and Details() immediately (un)marshalling the detail as
// an Any field, and since gogoproto types are not registered with the standard
// Protobuf registry (and vice versa) they cannot be unmarshalled.
//
// Furthermore, since we have to encode all errors as gogoproto Status
// messages to place them in EncodedError (again because of an Any field),
// only gogoproto status details can be preserved across EncodeError().
// See also: https://github.com/cockroachdb/errors/issues/63

package extgrpc

import (
	"context"
	"fmt"

	"github.com/cockroachdb/errors"
	"github.com/cockroachdb/errors/errbase"
	"github.com/cockroachdb/errors/markers"
	"github.com/cockroachdb/redact"
	gogorpc "github.com/gogo/googleapis/google/rpc"
	"github.com/gogo/protobuf/proto"
	gogostatus "github.com/gogo/status"
	"google.golang.org/grpc/codes"
	grpcstatus "google.golang.org/grpc/status"
)

// withGrpcCode wraps an error with a gRPC status code.
type withGrpcCode struct {
	cause error
	code  codes.Code
}

// WrapWithGrpcCode wraps an error with a gRPC status code.
func WrapWithGrpcCode(err error, code codes.Code) error {
	if err == nil {
		return nil
	}
	return &withGrpcCode{cause: err, code: code}
}

// GetGrpcCode retrieves the gRPC code from a stack of causes.
func GetGrpcCode(err error) codes.Code {
	if err == nil {
		return codes.OK
	}
	if v, ok := markers.If(err, func(err error) (interface{}, bool) {
		if w, ok := err.(*withGrpcCode); ok {
			return w.code, true
		}
		return nil, false
	}); ok {
		return v.(codes.Code)
	}
	return codes.Unknown
}

// it's an error.
func (w *withGrpcCode) Error() string { return w.cause.Error() }

// it's also a wrapper.
func (w *withGrpcCode) Cause() error  { return w.cause }
func (w *withGrpcCode) Unwrap() error { return w.cause }

// it knows how to format itself.
func (w *withGrpcCode) Format(s fmt.State, verb rune) { errors.FormatError(w, s, verb) }

// SafeFormatter implements errors.SafeFormatter.
// Note: see the documentation of errbase.SafeFormatter for details
// on how to implement this. In particular beware of not emitting
// unsafe strings.
func (w *withGrpcCode) SafeFormatError(p errors.Printer) (next error) {
	if p.Detail() {
		p.Printf("gRPC code: %s", redact.Safe(w.code))
	}
	return w.cause
}

// it's an encodable error.
func encodeWithGrpcCode(_ context.Context, err error) (string, []string, proto.Message) {
	w := err.(*withGrpcCode)
	details := []string{fmt.Sprintf("gRPC %d", w.code)}
	payload := &EncodedGrpcCode{Code: uint32(w.code)}
	return "", details, payload
}

// it's a decodable error.
func decodeWithGrpcCode(
	_ context.Context, cause error, _ string, _ []string, payload proto.Message,
) error {
	wp := payload.(*EncodedGrpcCode)
	return &withGrpcCode{cause: cause, code: codes.Code(wp.Code)}
}

// encodeGrpcStatus takes an error generated by a standard gRPC Status and
// converts it into a GoGo Protobuf representation from
// github.com/gogo/googleapis/google/rpc.
//
// This is necessary since EncodedError uses an Any field for structured errors,
// and thus can only contain Protobufs that have been registered with the GoGo
// Protobuf type registry -- the standard gRPC Status type is not a GoGo
// Protobuf, and is therefore not registered with it and cannot be decoded by
// DecodeError().
//
// Also note that in order to use error details, the input type must be a
// gogoproto Status from github.com/gogo/status, not from the standard gRPC
// Status, and all details must be gogoproto types. The reasons for this
// are the same as for the Any field issue mentioned above.
func encodeGrpcStatus(_ context.Context, err error) (string, []string, proto.Message) {
	s := gogostatus.Convert(err)
	// If there are known safe details, return them.
	details := []string{}
	for _, detail := range s.Details() {
		if safe, ok := detail.(errbase.SafeDetailer); ok {
			details = append(details, safe.SafeDetails()...)
		}
	}
	return s.Message(), details, s.Proto()
}

// decodeGrpcStatus is the inverse of encodeGrpcStatus. It takes a gogoproto
// Status as input, and converts it into a standard gRPC Status error.
func decodeGrpcStatus(
	ctx context.Context, msg string, details []string, payload proto.Message,
) error {
	return grpcstatus.Convert(decodeGoGoStatus(ctx, msg, details, payload)).Err()
}

// encodeGoGoStatus encodes a GoGo Status error. It calls encodeGrpcStatus, since
// it can handle both kinds.
func encodeGoGoStatus(ctx context.Context, err error) (string, []string, proto.Message) {
	return encodeGrpcStatus(ctx, err)
}

// decodeGoGoStatus is similar to decodeGrpcStatus, but decodes into a gogo
// Status error instead of a gRPC Status error. It is used when the original
// error was a gogo Status error rather than a gRPC Status error.
func decodeGoGoStatus(_ context.Context, _ string, _ []string, payload proto.Message) error {
	s, ok := payload.(*gogorpc.Status)
	if !ok {
		// If input type was unexpected (shouldn't happen), we just return nil
		// which will cause DecodeError() to return an opaqueLeaf.
		return nil
	}
	return gogostatus.ErrorProto(s)
}

func init() {
	grpcError := grpcstatus.Error(codes.Unknown, "")
	errbase.RegisterLeafEncoder(errbase.GetTypeKey(grpcError), encodeGrpcStatus)
	errbase.RegisterLeafDecoder(errbase.GetTypeKey(grpcError), decodeGrpcStatus)

	gogoError := gogostatus.Error(codes.Unknown, "")
	errbase.RegisterLeafEncoder(errbase.GetTypeKey(gogoError), encodeGoGoStatus)
	errbase.RegisterLeafDecoder(errbase.GetTypeKey(gogoError), decodeGoGoStatus)

	errbase.RegisterWrapperEncoder(errbase.GetTypeKey((*withGrpcCode)(nil)), encodeWithGrpcCode)
	errbase.RegisterWrapperDecoder(errbase.GetTypeKey((*withGrpcCode)(nil)), decodeWithGrpcCode)
}
